package internalAPI.impl;
import java.io.UnsupportedEncodingException;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import org.xmldb.api.base.Collection;
import org.exist.util.serializer.SAXSerializer;
import org.exist.util.serializer.SerializerPool;
import org.xmldb.api.DatabaseManager;
import org.xmldb.api.base.CompiledExpression;
import org.xmldb.api.base.Database;
import org.xmldb.api.base.ResourceSet;
import org.xmldb.api.base.XMLDBException;
import org.xmldb.api.modules.XQueryService;
import internalAPI.*;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.xmldb.api.base.ResourceIterator;
import org.xmldb.api.modules.XMLResource;
import org.xmldb.api.modules.XUpdateQueryService;

/**
 * This class serves as an example query processor for the eXist implementation of xmldb
 * @author Lukas Rucka, Masaryk University, Brno, Czech Republic, xrucka@fi.muni.cz
 */
public class ExistServer implements QueryExecutor {

    // XML-DB related stuff
    private XQueryService queryService;
    private XUpdateQueryService updateService;
    private Database database;

    // Input KML transformation to XUpdate
    private Source transformSource;
    private Transformer transformer;

    /**
     * Constructs server for given database url
     * @param uri - uri of the database in form <server>:<port>/<path>
     * @throws ClassNotFoundException if eXist driver missing
     * @throws InstantiationException if eXist driver damaged/incompatible
     * @throws IllegalAccessException if target database access restricted
     */
    public ExistServer(String uri) throws ClassNotFoundException, InstantiationException, IllegalAccessException{

        // transformation related stuff
        transformSource = null;
        transformer = null;
        {
            // the KML->XUpdate transformation template is distributed within the jar archive
            InputStream stylesheet = this.getClass().getResourceAsStream("/internalAPI/impl/resources/XUpdateStylesheet.xsl");
            if (stylesheet != null){
                transformSource = new StreamSource(stylesheet);
                TransformerFactory tfactory = TransformerFactory.newInstance();
                try {
                    transformer = tfactory.newTransformer(transformSource);
                } catch (TransformerConfigurationException ex) {
                    Logger.getLogger(ExistServer.class.getName()).log(Level.SEVERE, null, ex);
                }
            } else {
                System.err.println("Unable to load XSLT template for XUpdate. Store functionality will not be supported!");
            }

        }

        // eXist init related stuff
        String driver = "org.exist.xmldb.DatabaseImpl";
        uri = "xmldb:exist://"+uri;
        try {
            Class cl = Class.forName(driver);
            database = (Database) cl.newInstance();
//          no-create-database!
//            database.setProperty("create-database", "true");
            DatabaseManager.registerDatabase(database);

            Collection col = DatabaseManager.getCollection(uri);

            // for select
            queryService = (XQueryService)(col.getService("XQueryService", "1.0"));
            queryService.setProperty(OutputKeys.INDENT, "yes");
            queryService.setProperty(OutputKeys.ENCODING, "UTF-8");

            // for update
            updateService = (XUpdateQueryService)(col.getService("XUpdateQueryService", "1.0"));
            updateService.setProperty(OutputKeys.INDENT, "yes");
            updateService.setProperty(OutputKeys.ENCODING, "UTF-8");

        } catch (ClassNotFoundException ex){
            System.err.println("Unable to use eXist");
            throw ex;
        } catch (InstantiationException ex){
            System.err.println("Unable to use eXist");
            throw ex;
        } catch (IllegalAccessException ex){
            System.err.println("Access denied");
            throw ex;
        } catch (XMLDBException ex){
            System.err.println("Unable to connect to eXist server");
        }


    }

    /**
     * Process the query and return result (intersection of conditions in query)
     * @param query - object containing the conditions for search
     * @return - set of entries that fit all the conditions from the query or empty kml if no corresponding entry found or an error occoured
     */
    @Override
    public String processQuery(Query query){



        String response = "<?xml version=\"1.0\" ?>\r\n<kml xmlns=\"http://www.opengis.net/kml/2.2\"><Document><name>Štatlař: an error has occoured</name></Document>\r\n</kml>\r\n";

        try {
            String queryString = setupQuery(query);

            CompiledExpression compiled = queryService.compile(queryString);

            ResourceSet result = queryService.execute(compiled);

            Properties outputProperties = new Properties();
            outputProperties.setProperty(OutputKeys.INDENT, "yes");

            // resulting KML is returned in DOM, yet we need it returned as string
            StringWriter outStream = new StringWriter();
            SAXSerializer serializer = (SAXSerializer) SerializerPool.getInstance().borrowObject(SAXSerializer.class);
            serializer.setOutput(outStream, outputProperties);

            // serialize the KML
            for (ResourceIterator resourceIterator = result.getIterator(); resourceIterator.hasMoreResources(); ) {
                XMLResource resource = (XMLResource) resourceIterator.nextResource();
                resource.getContentAsSAX(serializer);
            }

            SerializerPool.getInstance().returnObject(serializer);

            // return resources and result
            response = outStream.toString();
            SerializerPool.getInstance().returnObject(serializer);

        } catch (XMLDBException ex){
            System.err.println(ex);
        }


        return response;
    }

    /**
     * Auxiliary function that sets up the XQuery string based on conditions given in argument
     * @param query - object containing the conditions for search
     * @return
     */
    private String setupQuery(Query query){

// what needs to be done: parse the coordinates string into set of points denoted by three floats
// write the latlng2metric function
/*                           + "declare function local:is_close_enough() as  {
   sum($po/order-item/(@price * @quantity))
};
*/
        boolean cond = false;
        String queryCond = "";

        if (query.getName() != null){
            if (cond){
                queryCond = queryCond + " AND ";
            }
            queryCond = queryCond + "contains(lower-case($placemark/name), lower-case(\""+query.getName()+"\"))";
            cond = true;
        }
//        if (query.getNearSearchCoordinates() != null){
//
//        }

        // TODO: Type is stored in snippet
        if (query.getType() != null){
            if (cond){
                queryCond = queryCond + " AND ";
            }
            queryCond = queryCond + "contains(lower-case($placemark/Snippet), lower-case(\""+query.getType()+"\"))";
            cond = true;
        }
        // TODO: Type is stored in snippet
        if (query.getFulltext() != null){
            if (cond){
                queryCond = queryCond + " AND ";
            }
            queryCond = queryCond + "contains(lower-case($placemark), lower-case(\""+query.getType()+"\"))";
            cond = true;
        }

        String queryString = "declare default element namespace \"http://www.opengis.net/kml/2.2\";\r\n"
                   + "<kml xmlns=\"http://www.opengis.net/kml/2.2\"><Document><name>Štatlař: query result</name>\r\n{\r\n"
                   + "for $placemark in /kml/Document/Placemark\r\n"
                   + ((cond)?("where ("+queryCond+")\r\n"):"")+"return $placemark\r\n}\r\n</Document></kml>\r\n";

        return queryString;
    }

    /**
     * Setups the update query XUpdate and returns it in it's string form
     * @param kml
     * @return
     */
    private String setupUpdate(String kml){
        InputStream sx = null;
        try {
            sx = new ByteArrayInputStream(kml.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException ex) {
            Logger.getLogger(ExistServer.class.getName()).log(Level.SEVERE, null, ex);
            sx = new ByteArrayInputStream(kml.getBytes());
        }

        if (sx == null){
            System.err.println("Failed to convert input string to input stream");
            return null;
        }

        Source srcKml = new StreamSource(sx);
        StringWriter writer = new StringWriter();
        Result res = new StreamResult(writer);
        try {
            transformer.transform(srcKml, res);
        } catch (TransformerException ex) {
            Logger.getLogger(ExistServer.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }

        // stupid, stupid XSLT default namespace problems!, however should be fixed now
        return writer.toString();//.replace("xmlns:xupdate=", "xmlns:kml2=\"http://www.opengis.net/kml/2.2\" xmlns:xupdate=");
    }

    /**
     * Stores the KML into the database
     * @param kml - KML (string) to store
     * @return true if successfully stored
     */
    @Override
    public boolean storePlace(String kml) {

        String updateQuery = setupUpdate(kml);

        if (updateQuery == null){ return false; }
        try {
            return (updateService.update(updateQuery) != 0);
        } catch (XMLDBException ex) {
            Logger.getLogger(ExistServer.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        }


    }


}
